package simtester.server

import grails.test.mixin.*
import groovy.mock.interceptor.MockFor
import java.util.concurrent.LinkedBlockingQueue
import org.junit.*

import simtester.cloud.Cloud
import simtester.cloud.CloudException

/**
 * See the API for {@link grails.test.mixin.services.ServiceUnitTestMixin} for usage instructions
 */
@TestFor(CloudService)
@Mock([User,TestSuite,TestCase,TestRun,ResultEntry,Instance,Team])
class CloudServiceTests {
    static config
    static testEnv = 'test/testenv'
    static scriptname = 'some script'
    static testcaseName = 'tc1'
    static testrunName = 'test1'
    
    def team
    static testuser
    static testcase
    
    @BeforeClass
    static void setUpClass() {
        config = new ConfigSlurper().parse(new File('grails-app/conf/SimTesterConfig.groovy').toURI().toURL())
    }
    
    @Before
    void setUp() {
        testuser = new User(username: 'testuser', password: 'pass')
        team = new Team(name: 'svt')
        team.addToUsers(testuser)

        testcase = new TestCase(name: testcaseName, owner: testuser, script: scriptname)
        team.addToTestcases(testcase)
        team.save(flush: true, failOnError: true)
    }
    
    @After
    void tearDown() {
    }

    @Test
    void runTestAgent() {
        def testInstanceId = "12345"
        def testAmiId = 'ami-1bd68a5e'
        def testType = 'm1.large'
        def testRepeats = 3
        
        new Instance(cloudId: testInstanceId, amiId: testAmiId, type: testType, status: 'free').save(flush:true,failOnError:true)
        
        def args = [:]
        def mockCloud = new MockFor(Cloud)
        mockCloud.demand.setupTestAgent { map ->
            args = map
            return [id: testInstanceId, amiId: testAmiId, type: testType]
        }

        service.cloud = mockCloud.proxyInstance(type: 'local', creds: config.aws.credentialsfile, endpoint: config.aws.endpoint)
        service.runTestAgent(testrunName: testrunName, testcaseId: testcase.id, repeats: testRepeats)
        service.creationThread.join()
        
        mockCloud.verify(service.cloud)
        assert args.instanceId == testInstanceId
        assert args.script  == scriptname
        assert args.url     == 'http://localhost:8080/server/'
        assert args.testrunId instanceof Long
        assert args.repeats == testRepeats
        assert !args.duration
        
        def tr = TestRun.get(args.testrunId)
        assert tr.repeats == testRepeats
        assert tr.users   == 1
        assert tr.name    == testrunName
        assert !tr.testDuration
        assert tr.instances.size() == 1

        def instance = Instance.list()[0]
        //assert instance.testrun.id.toString() == args.testrunId
        assert instance.cloudId == testInstanceId
        assert instance.amiId   == testAmiId
        assert instance.type    == testType
        assert instance.status  == 'busy'
    }

    @Test
    void runTestAgentForTestSuite() {
        def testAmiId = 'ami-1bd68a5e'
        def testType = 'm1.large'
        def testRepeats = 3
        
        5.times {
            new Instance(cloudId: '1234'+it, amiId: testAmiId, type: testType, status: 'free').save(flush:true,failOnError:true)
        }

        def tests = []
        3.times {
            def tc = new TestCase(name: 'mytest'+it, owner: testuser, script: scriptname+it)
            team.addToTestcases(tc)
            tests << tc
        }
        team.save(flush:true, failOnError:true)
        
        def testsuite = new TestSuite(name: 'suite', type: 'GUI', owner: testuser)
        team.addToTestsuites(testsuite)
        team.save(flush:true, failOnError:true)
        
        tests.each {
            testsuite.addToTestcases(it)
        }
        testsuite.save(flush:true, failOnError:true)
        
        def concurrentArgs = new LinkedBlockingQueue()
        def mockCloud = new MockFor(Cloud)
        mockCloud.demand.setupTestAgent(5) { map ->
            concurrentArgs << map
        }
        
        def users = [3, 26, 30]
        
        service.cloud = mockCloud.proxyInstance(type: 'local', creds: config.aws.credentialsfile, endpoint: config.aws.endpoint)
        service.runTestAgent(testrunName: testrunName,
            testsuiteId: testsuite.id,
            repeats: testRepeats,
            users: [
                (tests[0].id.toString()): users[0],
                (tests[1].id.toString()): users[1],
                (tests[2].id.toString()): users[2],
            ]
        )
        service.creationThread.join()
        
        def args = []
        concurrentArgs.size().times {
            args << concurrentArgs.take()
        }

        assert args.every {
            it.url == 'http://localhost:8080/server/' &&
            it.testrunId instanceof Long &&
            it.repeats   == testRepeats &&
            !it.duration
        }
        
        assert 1 == args.count {
            it.script     == scriptname + '0' &&
            it.testcaseId == tests[0].id &&
            it.firstuser  == 1 &&
            it.users      == 3
        }
        
        assert 1 == args.count {
            it.script     == scriptname + '1' &&
            it.testcaseId == tests[1].id &&
            it.firstuser  == 1 &&
            it.users      == 25
        }
        
        assert 1 == args.count {
            it.script     == scriptname + '1' &&
            it.testcaseId == tests[1].id &&
            it.firstuser  == 26 &&
            it.users      == 1
        }
        
        assert 1 == args.count {
            it.script     == scriptname + '2' &&
            it.testcaseId == tests[2].id &&
            it.firstuser  == 1 &&
            it.users      == 25
        }
        
        assert 1 == args.count {
            it.script     == scriptname + '2' &&
            it.testcaseId == tests[2].id &&
            it.firstuser  == 26 &&
            it.users      == 5
        }

        def tr = TestRun.get(args[0].testrunId)
        assert tr.repeats == testRepeats
        assert tr.name    == testrunName
        
        assert !tr.testDuration
        assert tr.instances.size() == 5
        
        def instances = Instance.list()
        assert instances.every {
            it.status == 'busy'
        }
    }
    
    @Test
    void runTestAgentWithDurationAndUsers() {
        def testInstanceId = "12345"
        def testAmiId = 'ami-1bd68a5e'
        def testType = 'm1.large'
        def testDuration = 2234352
        def testUsers = 3
        def args = [:]
        def mockCloud = new MockFor(Cloud)
        mockCloud.demand.setupTestAgent { map ->
            args = map
            return [id: testInstanceId, amiId: testAmiId, type: testType]
        }
        
        new Instance(cloudId: testInstanceId, amiId: testAmiId, type: testType, status: 'free').save(flush:true,failOnError:true)

        service.cloud = mockCloud.proxyInstance(type: 'local', creds: config.aws.credentialsfile, endpoint: config.aws.endpoint)
        service.runTestAgent(testrunName: testrunName, testcaseId: testcase.id, duration: testDuration, users: testUsers)
        service.creationThread.join()
        
        mockCloud.verify(service.cloud)
        assert args.script    == scriptname
        assert args.url       == 'http://localhost:8080/server/'
        assert args.testrunId instanceof Long
        assert !args.repeats
        assert args.duration  == testDuration
        assert args.firstuser == 1
        assert args.users     == testUsers
        
        def tr = TestRun.get(args.testrunId)
        assert tr.testDuration == testDuration
        assert !tr.repeats
        assert tr.users == testUsers
        assert tr.name == testrunName
        assert tr.instances.size() == 1
        
        def instance = Instance.list()[0]
        assert instance.testrun.id == args.testrunId
        assert instance.cloudId == testInstanceId
        assert instance.amiId   == testAmiId
        assert instance.type    == testType
        assert instance.status  == 'busy'
    }
    
    @Test
    void runTestAgentWithMultipleInstances() {
        def testInstanceId = "12345"
        def testAmiId = 'ami-1bd68a5e'
        def testType = 'm1.large'
        def testRepeats = 3
        def testUsers = 110
        def concurrentArgs = new LinkedBlockingQueue()
        def mockCloud = new MockFor(Cloud)
        mockCloud.demand.setupTestAgent(5) { map ->
            concurrentArgs << map
            return [id: testInstanceId, amiId: testAmiId, type: testType]
        }

        5.times {
            new Instance(cloudId: testInstanceId, amiId: testAmiId, type: testType, status: 'free').save(flush:true,failOnError:true)
        }

        service.cloud = mockCloud.proxyInstance(type: 'local', creds: config.aws.credentialsfile, endpoint: config.aws.endpoint)
        service.runTestAgent(testrunName: testrunName, testcaseId: testcase.id, users: testUsers, repeats: testRepeats)
        service.creationThread.join()
        
        def args = []
        concurrentArgs.size().times {
            args << concurrentArgs.take()
        }

        def count = args.count {
            it.repeats == testRepeats
            it.users   == 25
        }
        assert count == 4
        
        [1, 26, 51, 76, 101].each { u ->
            assert args.any { it.firstuser == u }
        }
        
        assert args.any { it.firstuser == 101 && it.users == 10 }
        
        def tr = TestRun.get(args[0].testrunId)
        assert tr.repeats == testRepeats
        assert tr.users == testUsers
        assert tr.name == testrunName
        assert tr.instances.size() == 5
        
        def instances = Instance.list()
        assert instances.every {
            it.status == 'busy'
        }
    }
    
    @Test
    void runLoadTestAgent() {
        def testInstanceId = "12345"
        def testAmiId = 'ami-1bd68a5e'
        def testType = 'm1.large'
        def concurrentArgs = new LinkedBlockingQueue()
        def mockCloud = new MockFor(Cloud)
        mockCloud.demand.setupTestAgent(4) { map ->
            concurrentArgs << map
            return [id: testInstanceId, amiId: testAmiId, type: testType]
        }
        
        4.times {
            new Instance(cloudId: testInstanceId, amiId: testAmiId, type: testType, status: 'free').save(flush:true,failOnError:true)
        }
        
        // Create test suite
        def tests = []
        2.times {
            def tc = new TestCase(name: 'mytest'+it, owner: testuser, script: scriptname+it)
            team.addToTestcases(tc)
            tests << tc
        }
        team.save(flush:true, failOnError:true)
        
        def testsuite = new TestSuite(name: 'suite', type: 'Load', owner: testuser)
        team.addToTestsuites(testsuite)
        team.save(flush:true, failOnError:true)
        
        tests.each {
            testsuite.addToTestcases(it)
        }
        testsuite.save(flush:true, failOnError:true)
        
        def tcList = tests.collect {
            [id: it.id]
        }
        
        tcList[0].phases = []
        tcList[0].phases << [
            users:    5,
            per:      20 * 1000,
            duration: 60 * 1000
        ]
        tcList[0].phases << [
            users:    2,
            per:      10 * 1000,
            duration: 25 * 1000
        ]
        tcList[1].phases = []
        tcList[1].phases << [
            users:    10,
            per:      10 * 1000,
            duration: 65 * 1000
        ]
        tcList[1].phases << [
            users:    20,
            per:      5 * 1000,
            duration: 2 * 1000
        ]
        
        service.cloud = mockCloud.proxyInstance(type: 'local', creds: config.aws.credentialsfile, endpoint: config.aws.endpoint)
        service.runLoadTestAgent(testrunName: testrunName, testsuiteId: testsuite.id.toString(), testcases: tcList)
        service.creationThread.join()
        
        def args = []
        concurrentArgs.size().times {
            args << concurrentArgs.take()
        }

        assert args.size() == 4
        tcList.each { tc ->
            tc.phases.each { phase ->
                assert args.any { arg ->
                    arg.peruser  == phase.users &&
                    arg.per      == phase.per &&
                    arg.duration == phase.duration &&
                    arg.url == 'http://localhost:8080/server/'
                }
            }
        }

        // testcaseId
        tcList.each { tc ->
            assert args.count { it.testcaseId == tc.id } == 2
        }
        
        // script
        tests.each { test ->
            assert args.count { it.script == test.script }
        }
        
        // total duration
        def totalduration = []
        tcList.each { tc ->
            def durcount = 0
            tc.phases.each { phase ->
                durcount += phase.duration
            }
            
            tc.phases.each { phase ->
                def arg = args.find { it.peruser == phase.users }
                assert arg.totalduration == durcount
            }
        }

        // delays
        // Testcase 1, Phase 1
        def arg = args.find { it.peruser == tcList[0].phases[0].users }
        assert arg.delay == 0
        assert arg.firstuser == 1

        // Testcase 1, Phase 2
        arg = args.find { it.peruser == tcList[0].phases[1].users }
        assert arg.delay == 60000
        assert arg.firstuser == 16
        
        // Testcase 2, Phase 1
        arg = args.find { it.peruser == tcList[1].phases[0].users }
        assert arg.delay == 0
        assert arg.firstuser == 1
        
        // Testcase 2, Phase 2
        arg = args.find { it.peruser == tcList[1].phases[1].users }
        assert arg.delay == 65000
        assert arg.firstuser == 71
        
        def tr = TestRun.get(args[0].testrunId)
        assert tr.name == testrunName
        assert tr.type == 'Load'
        assert tr.testDuration == 85000
        assert tr.testcaseIds.size() == tcList.size()
        tcList.each { tc ->
            assert tr.testcaseIds.find { it == tc.id }
        }
        assert tr.instances.size() == 4
    }

    @Test
    void removeAllTestRunInstances() {
        def tr = new TestRun(name: testrunName, testcase: testcase, status: 'complete')
        def instanceList = [
            new Instance(testrun: tr, cloudId: 'i-39555b7e', amiId: 'ami-1bd68a5e', type: 'm1.large'),
            new Instance(testrun: tr, cloudId: 'i-39555b7f', amiId: 'ami-1bd68a5e', type: 'm1.large')
        ]
        tr.addToInstances( instanceList[0] )
        tr.addToInstances( instanceList[1] )
        tr.save()

        /*def mockCloud = new MockFor(Cloud)
        def removeInstancesArgs = [:]
        mockCloud.demand.removeInstances { instances ->
            removeInstancesArgs.instances = instances
        }*/
        
        //service.cloud = mockCloud.proxyInstance(type: 'local', creds: config.aws.credentialsfile, endpoint: config.aws.endpoint)
        service.scheduleRemovalOfInstances(TestRun.findById(tr.id))
        service.removalThread.join()
        
        //mockCloud.verify(service.cloud)
        /*instanceList.eachWithIndex { inst, i ->
            assert removeInstancesArgs.instances[i] == inst.cloudId
        }*/

        def found = TestRun.findById(tr.id)
        assert found.testcase.name == testcaseName
        assert found.instances.isEmpty()
        
        def instance = Instance.list()[0]
        assert instance.status == 'free'
    }
    
    @Test
    void exceptionsFromAgentCreationFailsTestRun() {
        def testrunId
        def errMsg = 'some error'
        def mockCloud = new MockFor(Cloud)
        mockCloud.demand.with {
            setupTestAgent { map ->
                testrunId = map.testrunId
                throw new CloudException(errMsg)
            }
            //removeInstances { instances ->  }
        }
        
        new Instance(cloudId: '12345', amiId: 'ami-1bd68a5e', type: 'm1.large', status: 'free').save(flush:true,failOnError:true)
        
        service.cloud = mockCloud.proxyInstance(type: 'local', creds: config.aws.credentialsfile, endpoint: config.aws.endpoint)
        service.runTestAgent(testrunName: testrunName, testcaseId: testcase.id)
        service.creationThread.join()
        sleep 2000
        service.removalThread.join()
        
        mockCloud.verify(service.cloud)
        def found = TestRun.get(testrunId)
        assert found.status    == 'failed'
        assert found.instances == null

        def results = ResultEntry.findAllByTestrun(found)
        assert results.find { it.entry =~ /Encountered error that caused test to fail: $errMsg/ }
        
        def instance = Instance.list()[0]
        assert instance.status  == 'free'
    }
    
    @Test
    void runTestAgentButNoFreeInstance() {
        def testInstanceId = "12345"
        def testAmiId = 'ami-1bd68a5e'
        def testType = 'm1.large'
        def testDuration = 2234352
        def testUsers = 3
        def args = [:]
        def mockCloud = new MockFor(Cloud)
        mockCloud.demand.createTestAgent { map ->
            args = map
            return [id: testInstanceId, amiId: testAmiId, type: testType]
        }

        service.cloud = mockCloud.proxyInstance(type: 'local', creds: config.aws.credentialsfile, endpoint: config.aws.endpoint)
        service.runTestAgent(testrunName: testrunName, testcaseId: testcase.id, duration: testDuration, users: testUsers)
        service.creationThread.join()
        
        mockCloud.verify(service.cloud)
        assert args.script    == scriptname
        assert args.url       == 'http://localhost:8080/server/'
        assert args.testrunId instanceof Long
        assert !args.repeats
        assert args.firstuser == 1
        assert args.users     == testUsers
        assert args.duration  == testDuration
        
        def tr = TestRun.get(args.testrunId)
        assert tr.testDuration == testDuration
        assert !tr.repeats
        assert tr.users == 3
        assert tr.name == testrunName
        assert tr.instances.size() == 1
        
        def instance = Instance.list()[0]
        assert instance.testrun.id == args.testrunId
        assert instance.cloudId == testInstanceId
        assert instance.amiId   == testAmiId
        assert instance.type    == testType
        assert instance.status  == 'busy'
    }
}

//TODO: Tests for empty TestCase and TestRun?
